为什么你需要ViewObject

WhyNeedViewObject

作者:李旺成
时间:2016年4月12日


这里使用了一个解析当前天气 JSON 字符串得到原始 Model 后,将该 Model 的数据展示到一个简单的页面上来进行演示。

先看下 Demo 的效果图:
天气展示 Demo

我理解的 VO

VOViewObjectViewModel。关于它的解释在 Android MVP 详解(下)中,我做过简要的阐述。这里,再说说我是怎么理解 VO 的。

VO,就是一切给 View 提供数据的对象。这个定义就很广泛了,所以我对 VO 做了如下的分类(下面会细说)。

VO 的实现方式

既然,所有给 View 提供数据的对象都可以称之为 VO,那么 VO 的来源或者说形式就很多了。我在这里根据 VO 的实现方式进行了分类,仅仅是一家之言,有疏漏之处,见谅。

1. 单独的 VO 类

Android MVP 详解(下)中建议专门建一个包 vo,用来存放该模块下的所有 VO 类。对于这一类,那就属于单独的 VO 类,或者更准确的说明是“独立的 VO 类”。

要使用这种类型的 VO,有一个问题,它是独立的类,那么就需要另外的对象给它提供数据。在这里我认为提供(传递)数据的方式,大致有如下两种:

A. 使用转换器

专门使用一个转换器类,来做原始 Model 到 VO 的转换。如示例项目中的 VOConverterUtil.java 类。(在这类里偷了个懒,直接调用了“构造方法中转换”的方式进行了转换)
还是看下代码吧:

1
2
3
4
5
6
7
public class VOConverterUtil {
public static WeatherVO getWeatherVOFromWeatherBean(WeatherBean weatherBean) {
// 这里偷个懒
WeatherVO weatherVO = new WeatherVO(weatherBean);
return weatherVO;
}
}

B. 构造方法中转换

这个很好理解,就是在构造方法中进行数据转换。代码很简单,直接看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public WeatherVO(WeatherBean weatherBean) {
if (weatherBean == null) return;
isSuccess = "ok".equals(weatherBean.getStatus());
int condCode = Integer.parseInt(weatherBean.getNow().getCond().getCode());
String condCodeColorStr = "";
if (condCode < 0) {
weatherInfoIcon = R.mipmap.ic_snow;
condCodeColorStr = "#000066";
} else if (condCode < 60) {
weatherInfoIcon = R.mipmap.ic_rain;
condCodeColorStr = "#009900";
} else if (condCode < 90) {
weatherInfoIcon = R.mipmap.ic_cloudy;
condCodeColorStr = "#993300";
} else {
weatherInfoIcon = R.mipmap.ic_sunshine;
condCodeColorStr = "#cccc00";
}
weatherInfoText = Html.fromHtml("<font color='"+condCodeColorStr+"'>"+weatherBean.getNow().getCond().getTxt()+"</font>");
relativeHumidity = "相对湿度:" + weatherBean.getNow().getHum();
int tmpInt = Integer.parseInt(weatherBean.getNow().getTmp());
if (tmpInt < 15) {
temperatureIcon = R.mipmap.ic_lowtemperature;
} else if (tmpInt < 33) {
temperatureIcon = R.mipmap.ic_thermophilic;
} else {
temperatureIcon = R.mipmap.ic_hightemperature;
}
airPressure = "气压:" + weatherBean.getNow().getPres();
precipitation = "降水量:" + weatherBean.getNow().getPcpn();
visibility = "能见度:" + weatherBean.getNow().getVis() + " KM";
windDirectionAngle = "风向角度:" + weatherBean.getNow().getWind().getDeg();
windDirection = "风向:" + weatherBean.getNow().getWind().getDir();
windPower = "风力:" + weatherBean.getNow().getWind().getSc();
windSpeed = "风速" + weatherBean.getNow().getWind().getSpd();
}

2. 实现接口成 VO

抽出单独的类,那么就多了一个类, Modle 如果很多的话,那不可避免 VO 的数量也会增加。有些人可能觉得没必要,这增加了项目复杂度(哈哈,任何的设计都有可能造成复杂度上升)。那么,这样,我们抽取一个接口,然后让原始 Model 去实现这个接口 —— 以后就可以“面向接口编程”了。

思路很简单,那么直接上代码吧:
抽取接口 IWeatherVO.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface IWeatherVO {

boolean isSuccess(); // "status": "ok", //接口状态
int getWeatherInfoIcon(); // "code": "100", //天气状况代码 假设 <0 下雪, < 60 雨,大于 >60 < 90 阴, > 90 晴
Spanned getWeatherInfoText(); // "txt": "晴" //天气状况描述 天气的文本描述
String getRelativeHumidity(); // "hum": "20%", //相对湿度(%)
int getTemperatureIcon(); // "tmp": "32", //温度 温度图标
String getAirPressure(); // "pres": "1001", //气压
String getPrecipitation(); // 降水量
String getVisibility(); // "vis": "10", //能见度(km)
String getWindDirectionAngle(); // "deg": "10", //风向(360度)
String getWindDirection(); // "dir": "北风", //风向
String getWindPower(); // "sc": "3级", //风力
String getWindSpeed(); // "spd": "15" //风速(kmph)

}

实现接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
public class WeatherBean implements IWeatherVO {

// 原始 Modle 中的字段都省略了,具体看源码吧
...

//==========实现 VO 接口==========
@Override
public boolean isSuccess() {
return "ok".equals(status);
}

@Override
public int getWeatherInfoIcon() {
int weatherInfoIcon;
int condCode = Integer.parseInt(getNow().getCond().getCode());
if (condCode < 0) {
weatherInfoIcon = R.mipmap.ic_snow;
} else if (condCode < 60) {
weatherInfoIcon = R.mipmap.ic_rain;
} else if (condCode < 90) {
weatherInfoIcon = R.mipmap.ic_cloudy;
} else {
weatherInfoIcon = R.mipmap.ic_sunshine;
}
return weatherInfoIcon;
}

@Override
public Spanned getWeatherInfoText() {
Spanned weatherInfoText;
int condCode = Integer.parseInt(getNow().getCond().getCode());
String condCodeColorStr = "";
if (condCode < 0) {
condCodeColorStr = "#000066";
} else if (condCode < 60) {
condCodeColorStr = "#009900";
} else if (condCode < 90) {
condCodeColorStr = "#993300";
} else {
condCodeColorStr = "#cccc00";
}
weatherInfoText = Html.fromHtml("<font color='"+condCodeColorStr+"'>"+getNow().getCond().getTxt()+"</font>");
return weatherInfoText;
}

@Override
public String getRelativeHumidity() {
return "相对湿度:" + getNow().getHum();
}

@Override
public int getTemperatureIcon() {
int temperatureIcon;
int tmpInt = Integer.parseInt(getNow().getTmp());
if (tmpInt < 15) {
temperatureIcon = R.mipmap.ic_lowtemperature;
} else if (tmpInt < 33) {
temperatureIcon = R.mipmap.ic_thermophilic;
} else {
temperatureIcon = R.mipmap.ic_hightemperature;
}
return temperatureIcon;
}

@Override
public String getAirPressure() {
return "气压:" + getNow().getPres();
}

@Override
public String getPrecipitation() {
return "降水量:" + getNow().getPcpn();
}

@Override
public String getVisibility() {
return "能见度:" + getNow().getVis() + " KM";
}

@Override
public String getWindDirectionAngle() {
return "风向角度:" + getNow().getWind().getDeg();
}

@Override
public String getWindDirection() {
return "风向:" + getNow().getWind().getDir();
}

@Override
public String getWindPower() {
return "风力:" + getNow().getWind().getSc();
}

@Override
public String getWindSpeed() {
return "风速" + getNow().getWind().getSpd();
}

}

3. 添加方法成 VO

这个就更简单了,那就是连接口都不抽取了,直接提供上述接口中的方法。这里就不赘述了,思路是和上面提取接口一致,所提供的方法,目的就是方便在 View 中直接使用。(这个在 Android MVP 详解(下)中讨论过,略)

4. 没有 VO

没有 VO,那就是根本不使用 VO。如果你的项目是 MVP 的,那么就在 Presenter 中做数据转换的工作,然后提供给 View 展示。

这对于很简单的 Model 和 简单的 View 是没有问题的,如果,Model 很复杂(字段很多,而且不能直接使用),那么 Presenter 的任务就会很重。

这里就不做演示了,很多人应该都在这么用,或者曾经是这么用的。

使用 VO 的好处

上面说了一堆 VO 的实现方式,但是就是没提使用 VO 到底有何益处;或者说 VO 存在的意义。下面就我个人的理解,谈谈我认为 VO 的好处。

统一命名习惯

很多时候数据来源是网络(服务器端),那么这就可能有一个问题。服务器端的命名习惯可能与客户端有很大区别,还有不同服务器端开发的命名习惯也可能不同(如:使用 PHP 开发的服务器程序和使用 Java 开发的服务器程序命名很可能就是不同的)。

简而言之,那就是服务器反给我们的字段和我们项目中的命名习惯不同,很多人说,这没办法啊!总不能让服务器改吧!

是的,客户端和服务器端的命名很难统一,有人会说,不统一就不统一,又不影响使用。确实,不影响使用,但是,我们追求完美不是(先从最基本的命名规范做起,哈哈)。

所以,从这个角度来考虑,我建议原始的 Model 那就按照接口文档来(当然,如果使用 Gson 的话,关于命名不统一还是可以解决的,有兴趣的可以自行 Google)。我们自己针对 View 定义一套 VO,这个可以完全按照我们自己的命名规范来,至少这里是统一的。

解耦 View 和 Model

解耦,这个就不用多说了吧!我都不直接使用你了,这还不是解耦,View 依赖的是 VO,而不再依赖原始的 Modle。关于解耦所带来的优点,这里就不详述了,一搜一堆…

铺平数据结构

铺平数据结构” —— 可以理解为将原来有多级(层级较深)的对象,转换为层级较浅的对象。

我曾在项目中遇到这样一个问题:有很多相似的页面,但是服务器端给的字段都是不同的,这就需要建立多个 Model 来解析服务器给的数据。考虑到页面基本一样,那就不需要提供多个页面了,直接用一个页面,往里面填充不同的数据就可以了。那么,问题来了,这会导致要写很多重复的填充 View 的代码,因为 Model 是不同的。

对于上述的问题,我的解决方案是,将页面中要使用的数据抽取为独立的 VO,该页面只需要从 VO 中获取数据即可。再就是,关于如何建立 Modle 去解析服务器数据的问题。这里,我只建了一个 Modle,将所有使用这个页面的接口中的返回字段都封装到一个 Modle 中。这得益于 Model 中多了字段,并不会影响 JSON 字符串到对象的转换(至少 Gson 是这样的)。

上面说的这个例子,也可以认为是“铺平了数据结构”。

在这个示例 Demo 中,可以很好的演示 —— “铺平数据结构” 。
先看下原始的 JSON 字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"status": "ok",
"now": {
"cond": {
"code": "100",
"txt": "晴"
},
"fl": "30",
"hum": "20%",
"pcpn": "0.0",
"pres": "1001",
"tmp": "32",
"vis": "10",
"wind": {
"deg": "10", //风向(360度)
"dir": "北风", //风向
"sc": "3级", //风力
"spd": "15" //风速(kmph)
}
}
}

看一下,上面的 JSON 字符串,如果需要获取风速,那么需要先访问 now,在访问 wind,然后才能获取到 spd 字段。在代码中就如下:

1
weatherBean.getNow().getWind().getSpd();

而在我们的 VO 中,可以直接取到:

1
2
3
4
// 数据已经转换过了,这里直接可以取到
public String getWindSpeed() {
return windSpeed;
}

减少可能的问题

其实,View 和 Model 的耦合就是一个很大的问题,哈哈,这个确实能解决。

还有一些问题可以得到避免,例如,减少 View 中对 Model 的取值的各种判断(当然 MVP 就能解决),避免 Model 中的数据异常导致 View 崩溃。

这里就不多说这个问题了,等你遇到的时候,自然就知道能够避免哪些问题了。(偷个懒,这个以后有机会再丰富吧)

VO 使用演示

直接看图吧,就不上 GIF 了。

VO Class 演示

VO Interface 演示

VO Method 演示

小结

没有可以解决一切问题的妙药,no magic。

关于上述 VO 的各种形式,需要根据具体的场景(项目)来区分,当然这也在很大程度上取决于个人的习惯以及项目的大小。

如果是比较大的项目,那么建议直接抽出一个 VO 包来,为每个 View 都提供单独的 VO 对象,这样也可以保证项目的统一性,不会破坏层之间的依赖。

如果是小项目,那么可以混着用,觉得哪种方式使用起来最方便,那就使用哪种吧!

还是那句话,没有一定之规,要依据使用场景来确定。

项目地址:
GitHub

坚持原创技术分享,您的支持将鼓励我继续创作!